ποΈGitΠ―ΡΠ°ποΈ
.specify/scripts/bash/common.sh 13865e7c8b7f9a47cd3b572bd44d8ec355b2d3a9 (13865e7c) Text, 25.81 KB
T8b949e#!/usr/bin/env bash
T8b949e# Common functions and variables for all scripts
T8b949e# Find repository root by searching upward for .specify directory
T8b949e# This is the primary marker for spec-kit projects
find_specify_rootTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3dirTff7b72=Ta5d6ff"Tffd700${Te6edf31Tff7b72:-Tff7b72$(Tffa657pwdTff7b72)Tffd700}Ta5d6ff"
T8b949e# Normalize to absolute path to prevent infinite loop with relative paths
T8b949e# Use -- to handle paths starting with - (e.g., -P, -L)
Te6edf3dirTff7b72=Ta5d6ff"Tff7b72$(Tffa657cd -- Ta5d6ff"Te6edf3$dirTa5d6ff" T79c0ff2>/dev/null Tff7b72&& Tffa657pwdTff7b72)Ta5d6ff" Tff7b72|| Tff7b72return T79c0ff1
Tffa657local Te6edf3prev_dirTff7b72=Ta5d6ff""
Tff7b72while trueTb4b4b4; Tff7b72do
Tff7b72if Tff7b72[ -d Ta5d6ff"Te6edf3$dirTa5d6ff/.specifyTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Te6edf3$dirTa5d6ff"
Tff7b72return T79c0ff0
Tff7b72fi
T8b949e# Stop if we've reached filesystem root or dirname stops changing
Tff7b72if Tff7b72[ Ta5d6ff"Te6edf3$dirTa5d6ff" Tff7b72= Ta5d6ff"/" Tff7b72] Tff7b72|| Tff7b72[ Ta5d6ff"Te6edf3$dirTa5d6ff" Tff7b72= Ta5d6ff"Te6edf3$prev_dirTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tff7b72break
Tff7b72fi
Te6edf3prev_dirTff7b72=Ta5d6ff"Te6edf3$dirTa5d6ff"
Te6edf3dirTff7b72=Ta5d6ff"Tff7b72$(dirname Ta5d6ff"Te6edf3$dirTa5d6ff"Tff7b72)Ta5d6ff"
Tff7b72done
Tff7b72return T79c0ff1
Tff7b72}
T8b949e# Get repository root, prioritizing .specify directory over git
T8b949e# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
get_repo_rootTff7b72(Tff7b72) Tff7b72{
T8b949e# First, look for .specify directory (spec-kit's own marker)
Tffa657local specify_root
Tff7b72if Te6edf3specify_rootTff7b72=Tff7b72$(find_specify_rootTff7b72)Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Te6edf3$specify_rootTa5d6ff"
Tff7b72return
Tff7b72fi
T8b949e# Fallback to git if no .specify found
Tff7b72if git rev-parse --show-toplevel >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
git rev-parse --show-toplevel
Tff7b72return
Tff7b72fi
T8b949e# Final fallback to script location for non-git repos
Tffa657local Te6edf3script_dirTff7b72=Ta5d6ff"Tff7b72$(Te6edf3CDPATHTff7b72=Ta5d6ff"" Tffa657cd Ta5d6ff"Tff7b72$(dirname Ta5d6ff"Tffd700${Te6edf3BASH_SOURCETb4b4b4[0]Tffd700}Ta5d6ff"Tff7b72)Ta5d6ff" Tff7b72&& Tffa657pwdTff7b72)Ta5d6ff"
Tff7b72(Tffa657cd Ta5d6ff"Te6edf3$script_dirTa5d6ff/../../..Ta5d6ff" Tff7b72&& Tffa657pwdTff7b72)
Tff7b72}
T8b949e# Get current branch, with fallback for non-git repositories
get_current_branchTff7b72(Tff7b72) Tff7b72{
T8b949e# First check if SPECIFY_FEATURE environment variable is set
Tff7b72if Tff7b72[Tff7b72[ -n Ta5d6ff"Tffd700${Te6edf3SPECIFY_FEATURETff7b72:-Tffd700}Ta5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Te6edf3$SPECIFY_FEATURETa5d6ff"
Tff7b72return
Tff7b72fi
T8b949e# Then check git if available at the spec-kit root (not parent)
Tffa657local Te6edf3repo_rootTff7b72=Tff7b72$(get_repo_rootTff7b72)
Tff7b72if has_gitTb4b4b4; Tff7b72then
git -C Ta5d6ff"Te6edf3$repo_rootTa5d6ff" rev-parse --abbrev-ref HEAD
Tff7b72return
Tff7b72fi
T8b949e# For non-git repos, try to find the latest feature directory
Tffa657local Te6edf3specs_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/specsTa5d6ff"
Tff7b72if Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$specs_dirTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3latest_featureTff7b72=Ta5d6ff""
Tffa657local Te6edf3highestTff7b72=T79c0ff0
Tffa657local Te6edf3latest_timestampTff7b72=Ta5d6ff""
Tff7b72for dir Tff7b72in Ta5d6ff"Te6edf3$specs_dirTa5d6ff"/*Tb4b4b4; Tff7b72do
Tff7b72if Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$dirTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3dirnameTff7b72=Tff7b72$(basename Ta5d6ff"Te6edf3$dirTa5d6ff"Tff7b72)
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$dirnameTa5d6ff" Tff7b72=~ ^Tff7b72(Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff8Tff7b72}-Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff6Tff7b72}Tff7b72)- Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# Timestamp-based branch: compare lexicographically
Tffa657local Te6edf3tsTff7b72=Ta5d6ff"Tffd700${Te6edf3BASH_REMATCHTb4b4b4[1]Tffd700}Ta5d6ff"
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$tsTa5d6ff" > Ta5d6ff"Te6edf3$latest_timestampTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3latest_timestampTff7b72=Ta5d6ff"Te6edf3$tsTa5d6ff"
Te6edf3latest_featureTff7b72=Te6edf3$dirname
Tff7b72fi
Tff7b72elif Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$dirnameTa5d6ff" Tff7b72=~ ^Tff7b72(Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff3,Tff7b72}Tff7b72)- Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3numberTff7b72=Tffd700${Te6edf3BASH_REMATCHTb4b4b4[1]Tffd700}
Te6edf3numberTff7b72=Tff7b72$((T79c0ff10#Te6edf3$numberTff7b72))
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$numberTa5d6ff" -gt Ta5d6ff"Te6edf3$highestTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3highestTff7b72=Te6edf3$number
T8b949e# Only update if no timestamp branch found yet
Tff7b72if Tff7b72[Tff7b72[ -z Ta5d6ff"Te6edf3$latest_timestampTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3latest_featureTff7b72=Te6edf3$dirname
Tff7b72fi
Tff7b72fi
Tff7b72fi
Tff7b72fi
Tff7b72done
Tff7b72if Tff7b72[Tff7b72[ -n Ta5d6ff"Te6edf3$latest_featureTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Te6edf3$latest_featureTa5d6ff"
Tff7b72return
Tff7b72fi
Tff7b72fi
Tffa657echo Ta5d6ff"main" T8b949e# Final fallback
Tff7b72}
T8b949e# Check if we have git available at the spec-kit root level
T8b949e# Returns true only if git is installed and the repo root is inside a git work tree
T8b949e# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
has_gitTff7b72(Tff7b72) Tff7b72{
T8b949e# First check if git command is available (before calling get_repo_root which may use git)
Tffa657command -v git >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1 Tff7b72|| Tff7b72return T79c0ff1
Tffa657local Te6edf3repo_rootTff7b72=Tff7b72$(get_repo_rootTff7b72)
T8b949e# Check if .git exists (directory or file for worktrees/submodules)
Tff7b72[ -e Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.gitTa5d6ff" Tff7b72] Tff7b72|| Tff7b72return T79c0ff1
T8b949e# Verify it's actually a valid git work tree
git -C Ta5d6ff"Te6edf3$repo_rootTa5d6ff" rev-parse --is-inside-work-tree >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1
Tff7b72}
T8b949e# Strip a single optional path segment (e.g. gitflow "feat/004-name" -> "004-name").
T8b949e# Only when the full name is exactly two slash-free segments; otherwise returns the raw name.
spec_kit_effective_branch_nameTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3rawTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$rawTa5d6ff" Tff7b72=~ ^Tff7b72(Tff7b72[^/Tff7b72]+Tff7b72)/Tff7b72(Tff7b72[^/Tff7b72]+Tff7b72)$ Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657printf Ta5d6ff'%s\n' Ta5d6ff"Tffd700${Te6edf3BASH_REMATCHTb4b4b4[2]Tffd700}Ta5d6ff"
Tff7b72else
Tffa657printf Ta5d6ff'%s\n' Ta5d6ff"Te6edf3$rawTa5d6ff"
Tff7b72fi
Tff7b72}
check_feature_branchTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3rawTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local Te6edf3has_git_repoTff7b72=Ta5d6ff"Te6edf3$2Ta5d6ff"
T8b949e# For non-git repos, we can't enforce branch naming but still provide output
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$has_git_repoTa5d6ff" !Tff7b72= Ta5d6ff"true" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"[specify] Warning: Git repository not detected; skipped branch validation" >Tb4b4b4&T79c0ff2
Tff7b72return T79c0ff0
Tff7b72fi
Tffa657local branch
Te6edf3branchTff7b72=Tff7b72$(spec_kit_effective_branch_name Ta5d6ff"Te6edf3$rawTa5d6ff"Tff7b72)
T8b949e# Accept sequential prefix (3+ digits) but exclude malformed timestamps
T8b949e# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
Tffa657local Te6edf3is_sequentialTff7b72=Tffa657false
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$branchTa5d6ff" Tff7b72=~ ^Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff3,Tff7b72}- Tff7b72]Tff7b72] Tff7b72&& Tff7b72[Tff7b72[ ! Ta5d6ff"Te6edf3$branchTa5d6ff" Tff7b72=~ ^Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff7Tff7b72}-Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff6Tff7b72}- Tff7b72]Tff7b72] Tff7b72&& Tff7b72[Tff7b72[ ! Ta5d6ff"Te6edf3$branchTa5d6ff" Tff7b72=~ ^Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff7,8Tff7b72}-Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff6Tff7b72}$ Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3is_sequentialTff7b72=Tffa657true
Tff7b72fi
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$is_sequentialTa5d6ff" !Tff7b72= Ta5d6ff"true" Tff7b72]Tff7b72] Tff7b72&& Tff7b72[Tff7b72[ ! Ta5d6ff"Te6edf3$branchTa5d6ff" Tff7b72=~ ^Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff8Tff7b72}-Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff6Tff7b72}- Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Ta5d6ffERROR: Not on a feature branch. Current branch: Te6edf3$rawTa5d6ff" >Tb4b4b4&T79c0ff2
Tffa657echo Ta5d6ff"Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >Tb4b4b4&T79c0ff2
Tff7b72return T79c0ff1
Tff7b72fi
Tff7b72return T79c0ff0
Tff7b72}
T8b949e# Safely read .specify/feature.json's "feature_directory" value.
T8b949e# Prints the raw value (possibly relative) to stdout, or empty string if the file
T8b949e# is missing, unparseable, or does not contain the key. Always returns 0 so callers
T8b949e# under `set -e` cannot be aborted by parser failure.
T8b949e# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
read_feature_json_feature_directoryTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3repo_rootTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local Te6edf3fjTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/feature.jsonTa5d6ff"
Tff7b72[Tff7b72[ -f Ta5d6ff"Te6edf3$fjTa5d6ff" Tff7b72]Tff7b72] Tff7b72|| Tff7b72{ Tffa657printf Ta5d6ff'%s' Ta5d6ff''Tb4b4b4; Tff7b72return T79c0ff0Tb4b4b4; Tff7b72}
Tffa657local Te6edf3_fdTff7b72=Ta5d6ff''
Tff7b72if Tffa657command -v jq >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
Tff7b72if ! Te6edf3_fdTff7b72=Tff7b72$(jq -r Ta5d6ff'.feature_directory // empty' Ta5d6ff"Te6edf3$fjTa5d6ff" T79c0ff2>/dev/nullTff7b72)Tb4b4b4; Tff7b72then
Te6edf3_fdTff7b72=Ta5d6ff''
Tff7b72fi
Tff7b72elif Tffa657command -v python3 >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
T8b949e# Use Python so pretty-printed/multi-line JSON still parses correctly.
Tff7b72if ! Te6edf3_fdTff7b72=Tff7b72$(python3 -c Ta5d6ff"import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" Ta5d6ff"Te6edf3$fjTa5d6ff" T79c0ff2>/dev/nullTff7b72)Tb4b4b4; Tff7b72then
Te6edf3_fdTff7b72=Ta5d6ff''
Tff7b72fi
Tff7b72else
T8b949e# Last-resort single-line grep/sed fallback. The `|| true` guards against
T8b949e# grep returning 1 (no match) aborting under `set -e` / `pipefail`.
Te6edf3_fdTff7b72=Tff7b72$( Tff7b72{ grep -E Ta5d6ff'"feature_directory"[[:space:]]*:' Ta5d6ff"Te6edf3$fjTa5d6ff" T79c0ff2>/dev/null Tff7b72|| trueTb4b4b4; Tff7b72} Tffea00\
Tb4b4b4| head -n T79c0ff1 Tffea00\
Tb4b4b4| sed -E Ta5d6ff's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' Tff7b72)
Tff7b72fi
Tffa657printf Ta5d6ff'%s' Ta5d6ff"Te6edf3$_fdTa5d6ff"
Tff7b72return T79c0ff0
Tff7b72}
T8b949e# Returns 0 when .specify/feature.json lists feature_directory that exists as a directory
T8b949e# and matches the resolved active FEATURE_DIR (so /speckit.plan can skip git branch pattern checks).
T8b949e# Delegates parsing to read_feature_json_feature_directory, which is safe under `set -e`.
feature_json_matches_feature_dirTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3repo_rootTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local Te6edf3active_feature_dirTff7b72=Ta5d6ff"Te6edf3$2Ta5d6ff"
Tffa657local _fd
Te6edf3_fdTff7b72=Tff7b72$(read_feature_json_feature_directory Ta5d6ff"Te6edf3$repo_rootTa5d6ff"Tff7b72)
Tff7b72[Tff7b72[ -n Ta5d6ff"Te6edf3$_fdTa5d6ff" Tff7b72]Tff7b72] Tff7b72|| Tff7b72return T79c0ff1
Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$_fdTa5d6ff" !Tff7b72= /* Tff7b72]Tff7b72] Tff7b72&& Te6edf3_fdTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/Te6edf3$_fdTa5d6ff"
Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$_fdTa5d6ff" Tff7b72]Tff7b72] Tff7b72|| Tff7b72return T79c0ff1
Tffa657local norm_json norm_active
Te6edf3norm_jsonTff7b72=Ta5d6ff"Tff7b72$(Tffa657cd -- Ta5d6ff"Te6edf3$_fdTa5d6ff" T79c0ff2>/dev/null Tff7b72&& Tffa657pwd -PTff7b72)Ta5d6ff" Tff7b72|| Tff7b72return T79c0ff1
Te6edf3norm_activeTff7b72=Ta5d6ff"Tff7b72$(Tffa657cd -- Ta5d6ff"Te6edf3$active_feature_dirTa5d6ff" T79c0ff2>/dev/null Tff7b72&& Tffa657pwd -PTff7b72)Ta5d6ff" Tff7b72|| Tff7b72return T79c0ff1
Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$norm_jsonTa5d6ff" Tff7b72=Tff7b72= Ta5d6ff"Te6edf3$norm_activeTa5d6ff" Tff7b72]Tff7b72]
Tff7b72}
T8b949e# Find feature directory by numeric prefix instead of exact branch match
T8b949e# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
find_feature_dir_by_prefixTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3repo_rootTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local branch_name
Te6edf3branch_nameTff7b72=Tff7b72$(spec_kit_effective_branch_name Ta5d6ff"Te6edf3$2Ta5d6ff"Tff7b72)
Tffa657local Te6edf3specs_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/specsTa5d6ff"
T8b949e# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
Tffa657local Te6edf3prefixTff7b72=Ta5d6ff""
Tff7b72if Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$branch_nameTa5d6ff" Tff7b72=~ ^Tff7b72(Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff8Tff7b72}-Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff6Tff7b72}Tff7b72)- Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3prefixTff7b72=Ta5d6ff"Tffd700${Te6edf3BASH_REMATCHTb4b4b4[1]Tffd700}Ta5d6ff"
Tff7b72elif Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$branch_nameTa5d6ff" Tff7b72=~ ^Tff7b72(Tff7b72[T79c0ff0-9Tff7b72]Tff7b72{T79c0ff3,Tff7b72}Tff7b72)- Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3prefixTff7b72=Ta5d6ff"Tffd700${Te6edf3BASH_REMATCHTb4b4b4[1]Tffd700}Ta5d6ff"
Tff7b72else
T8b949e# If branch doesn't have a recognized prefix, fall back to exact match
Tffa657echo Ta5d6ff"Te6edf3$specs_dirTa5d6ff/Te6edf3$branch_nameTa5d6ff"
Tff7b72return
Tff7b72fi
T8b949e# Search for directories in specs/ that start with this prefix
Tffa657local Te6edf3matchesTff7b72=Tff7b72(Tff7b72)
Tff7b72if Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$specs_dirTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Tff7b72for dir Tff7b72in Ta5d6ff"Te6edf3$specs_dirTa5d6ff"/Ta5d6ff"Te6edf3$prefixTa5d6ff"-*Tb4b4b4; Tff7b72do
Tff7b72if Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$dirTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3matchesTff7b72+=Tff7b72(Ta5d6ff"Tff7b72$(basename Ta5d6ff"Te6edf3$dirTa5d6ff"Tff7b72)Ta5d6ff"Tff7b72)
Tff7b72fi
Tff7b72done
Tff7b72fi
T8b949e# Handle results
Tff7b72if Tff7b72[Tff7b72[ Tffd700${#Te6edf3matchesTb4b4b4[@]Tffd700} -eq T79c0ff0 Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# No match found - return the branch name path (will fail later with clear error)
Tffa657echo Ta5d6ff"Te6edf3$specs_dirTa5d6ff/Te6edf3$branch_nameTa5d6ff"
Tff7b72elif Tff7b72[Tff7b72[ Tffd700${#Te6edf3matchesTb4b4b4[@]Tffd700} -eq T79c0ff1 Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# Exactly one match - perfect!
Tffa657echo Ta5d6ff"Te6edf3$specs_dirTa5d6ff/Tffd700${Te6edf3matchesTb4b4b4[0]Tffd700}Ta5d6ff"
Tff7b72else
T8b949e# Multiple matches - this shouldn't happen with proper naming convention
Tffa657echo Ta5d6ff"Ta5d6ffERROR: Multiple spec directories found with prefix 'Te6edf3$prefixTa5d6ff': Tffd700${Te6edf3matchesTb4b4b4[*]Tffd700}Ta5d6ff" >Tb4b4b4&T79c0ff2
Tffa657echo Ta5d6ff"Please ensure only one spec directory exists per prefix." >Tb4b4b4&T79c0ff2
Tff7b72return T79c0ff1
Tff7b72fi
Tff7b72}
get_feature_pathsTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3repo_rootTff7b72=Tff7b72$(get_repo_rootTff7b72)
Tffa657local Te6edf3current_branchTff7b72=Tff7b72$(get_current_branchTff7b72)
Tffa657local Te6edf3has_git_repoTff7b72=Ta5d6ff"false"
Tff7b72if has_gitTb4b4b4; Tff7b72then
Te6edf3has_git_repoTff7b72=Ta5d6ff"true"
Tff7b72fi
T8b949e# Resolve feature directory. Priority:
T8b949e# 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
T8b949e# 2. .specify/feature.json "feature_directory" key (persisted by /speckit.specify)
T8b949e# 3. Branch-name-based prefix lookup (legacy fallback)
Tffa657local feature_dir
Tff7b72if Tff7b72[Tff7b72[ -n Ta5d6ff"Tffd700${Te6edf3SPECIFY_FEATURE_DIRECTORYTff7b72:-Tffd700}Ta5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3feature_dirTff7b72=Ta5d6ff"Te6edf3$SPECIFY_FEATURE_DIRECTORYTa5d6ff"
T8b949e# Normalize relative paths to absolute under repo root
Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$feature_dirTa5d6ff" !Tff7b72= /* Tff7b72]Tff7b72] Tff7b72&& Te6edf3feature_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/Te6edf3$feature_dirTa5d6ff"
Tff7b72elif Tff7b72[Tff7b72[ -f Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/feature.jsonTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# Shared, set -e-safe parser: jq -> python3 -> grep/sed. Returns empty on
T8b949e# missing/unparseable/unset so we fall through to the branch-prefix lookup.
Tffa657local _fd
Te6edf3_fdTff7b72=Tff7b72$(read_feature_json_feature_directory Ta5d6ff"Te6edf3$repo_rootTa5d6ff"Tff7b72)
Tff7b72if Tff7b72[Tff7b72[ -n Ta5d6ff"Te6edf3$_fdTa5d6ff" Tff7b72]Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3feature_dirTff7b72=Ta5d6ff"Te6edf3$_fdTa5d6ff"
T8b949e# Normalize relative paths to absolute under repo root
Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$feature_dirTa5d6ff" !Tff7b72= /* Tff7b72]Tff7b72] Tff7b72&& Te6edf3feature_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/Te6edf3$feature_dirTa5d6ff"
Tff7b72elif ! Te6edf3feature_dirTff7b72=Tff7b72$(find_feature_dir_by_prefix Ta5d6ff"Te6edf3$repo_rootTa5d6ff" Ta5d6ff"Te6edf3$current_branchTa5d6ff"Tff7b72)Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"ERROR: Failed to resolve feature directory" >Tb4b4b4&T79c0ff2
Tff7b72return T79c0ff1
Tff7b72fi
Tff7b72elif ! Te6edf3feature_dirTff7b72=Tff7b72$(find_feature_dir_by_prefix Ta5d6ff"Te6edf3$repo_rootTa5d6ff" Ta5d6ff"Te6edf3$current_branchTa5d6ff"Tff7b72)Tb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"ERROR: Failed to resolve feature directory" >Tb4b4b4&T79c0ff2
Tff7b72return T79c0ff1
Tff7b72fi
T8b949e# Use printf '%q' to safely quote values, preventing shell injection
T8b949e# via crafted branch names or paths containing special characters
Tffa657printf Ta5d6ff'REPO_ROOT=%q\n' Ta5d6ff"Te6edf3$repo_rootTa5d6ff"
Tffa657printf Ta5d6ff'CURRENT_BRANCH=%q\n' Ta5d6ff"Te6edf3$current_branchTa5d6ff"
Tffa657printf Ta5d6ff'HAS_GIT=%q\n' Ta5d6ff"Te6edf3$has_git_repoTa5d6ff"
Tffa657printf Ta5d6ff'FEATURE_DIR=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff"
Tffa657printf Ta5d6ff'FEATURE_SPEC=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/spec.mdTa5d6ff"
Tffa657printf Ta5d6ff'IMPL_PLAN=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/plan.mdTa5d6ff"
Tffa657printf Ta5d6ff'TASKS=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/tasks.mdTa5d6ff"
Tffa657printf Ta5d6ff'RESEARCH=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/research.mdTa5d6ff"
Tffa657printf Ta5d6ff'DATA_MODEL=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/data-model.mdTa5d6ff"
Tffa657printf Ta5d6ff'QUICKSTART=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/quickstart.mdTa5d6ff"
Tffa657printf Ta5d6ff'CONTRACTS_DIR=%q\n' Ta5d6ff"Te6edf3$feature_dirTa5d6ff/contractsTa5d6ff"
Tff7b72}
T8b949e# Check if jq is available for safe JSON construction
has_jqTff7b72(Tff7b72) Tff7b72{
Tffa657command -v jq >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1
Tff7b72}
T8b949e# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
T8b949e# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
json_escapeTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3sTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Tffea00\\Tb4b4b4/Tffea00\\Tffea00\\Tffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Tffea00\"Tb4b4b4/Tffea00\\Tffea00\"Tffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Ta5d6ff$'\n'Tb4b4b4/Tffea00\\Te6edf3nTffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Ta5d6ff$'\t'Tb4b4b4/Tffea00\\Te6edf3tTffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Ta5d6ff$'\r'Tb4b4b4/Tffea00\\Te6edf3rTffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Ta5d6ff$'\b'Tb4b4b4/Tffea00\\Te6edf3bTffd700}Ta5d6ff"
Te6edf3sTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4//Ta5d6ff$'\f'Tb4b4b4/Tffea00\\Te6edf3fTffd700}Ta5d6ff"
T8b949e# Escape any remaining U+0001-U+001F control characters as \uXXXX.
T8b949e# (U+0000/NUL cannot appear in bash strings and is excluded.)
T8b949e# LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
T8b949e# so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
Tffa657local Te6edf3LC_ALLTff7b72=C
Tffa657local i char code
Tff7b72for Tff7b72(Tff7b72( Te6edf3iTff7b72=T79c0ff0Tb4b4b4; i<Tffd700${#Te6edf3sTffd700}Tb4b4b4; i++ Tff7b72)Tff7b72)Tb4b4b4; Tff7b72do
Te6edf3charTff7b72=Ta5d6ff"Tffd700${Te6edf3sTb4b4b4:Te6edf3$iTb4b4b4:Te6edf31Tffd700}Ta5d6ff"
Tffa657printf -v code Ta5d6ff'%d' Ta5d6ff"Ta5d6ff'Te6edf3$charTa5d6ff" T79c0ff2>/dev/null Tff7b72|| Te6edf3codeTff7b72=T79c0ff256
Tff7b72if Tff7b72(Tff7b72( code >Tff7b72= T79c0ff1 Tff7b72&& code <Tff7b72= T79c0ff31 Tff7b72)Tff7b72)Tb4b4b4; Tff7b72then
Tffa657printf Ta5d6ff'\\u%04x' Ta5d6ff"Te6edf3$codeTa5d6ff"
Tff7b72else
Tffa657printf Ta5d6ff'%s' Ta5d6ff"Te6edf3$charTa5d6ff"
Tff7b72fi
Tff7b72done
Tff7b72}
check_fileTff7b72(Tff7b72) Tff7b72{ Tff7b72[Tff7b72[ -f Ta5d6ff"Te6edf3$1Ta5d6ff" Tff7b72]Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Ta5d6ff β Te6edf3$2Ta5d6ff" Tff7b72|| Tffa657echo Ta5d6ff"Ta5d6ff β Te6edf3$2Ta5d6ff"Tb4b4b4; Tff7b72}
check_dirTff7b72(Tff7b72) Tff7b72{ Tff7b72[Tff7b72[ -d Ta5d6ff"Te6edf3$1Ta5d6ff" Tff7b72&& -n Tff7b72$(ls -A Ta5d6ff"Te6edf3$1Ta5d6ff" T79c0ff2>/dev/nullTff7b72) Tff7b72]Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Ta5d6ff β Te6edf3$2Ta5d6ff" Tff7b72|| Tffa657echo Ta5d6ff"Ta5d6ff β Te6edf3$2Ta5d6ff"Tb4b4b4; Tff7b72}
T8b949e# Resolve a template name to a file path using the priority stack:
T8b949e# 1. .specify/templates/overrides/
T8b949e# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
T8b949e# 3. .specify/extensions/<ext-id>/templates/
T8b949e# 4. .specify/templates/ (core)
resolve_templateTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3template_nameTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local Te6edf3repo_rootTff7b72=Ta5d6ff"Te6edf3$2Ta5d6ff"
Tffa657local Te6edf3baseTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/templatesTa5d6ff"
T8b949e# Priority 1: Project overrides
Tffa657local Te6edf3overrideTff7b72=Ta5d6ff"Te6edf3$baseTa5d6ff/overrides/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$overrideTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$overrideTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
T8b949e# Priority 2: Installed presets (sorted by priority from .registry)
Tffa657local Te6edf3presets_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/presetsTa5d6ff"
Tff7b72if Tff7b72[ -d Ta5d6ff"Te6edf3$presets_dirTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3registry_fileTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/.registryTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$registry_fileTa5d6ff" Tff7b72] Tff7b72&& Tffa657command -v python3 >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
T8b949e# Read preset IDs sorted by priority (lower number = higher precedence).
T8b949e# The python3 call is wrapped in an if-condition so that set -e does not
T8b949e# abort the function when python3 exits non-zero (e.g. invalid JSON).
Tffa657local Te6edf3sorted_presetsTff7b72=Ta5d6ff""
Tff7b72if Te6edf3sorted_presetsTff7b72=Tff7b72$(Te6edf3SPECKIT_REGISTRYTff7b72=Ta5d6ff"Te6edf3$registry_fileTa5d6ff" python3 -c Ta5d6ff"
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" T79c0ff2>/dev/nullTff7b72)Tb4b4b4; Tff7b72then
Tff7b72if Tff7b72[ -n Ta5d6ff"Te6edf3$sorted_presetsTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# python3 succeeded and returned preset IDs β search in priority order
Tff7b72while Te6edf3IFSTff7b72= Tffa657read -r preset_idTb4b4b4; Tff7b72do
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/Te6edf3$preset_idTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
Tff7b72done Tff7b72<<< Ta5d6ff"Te6edf3$sorted_presetsTa5d6ff"
Tff7b72fi
T8b949e# python3 succeeded but registry has no presets β nothing to search
Tff7b72else
T8b949e# python3 failed (missing, or registry parse error) β fall back to unordered directory scan
Tff7b72for preset Tff7b72in Ta5d6ff"Te6edf3$presets_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$presetTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$presetTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
Tff7b72done
Tff7b72fi
Tff7b72else
T8b949e# Fallback: alphabetical directory order (no python3 available)
Tff7b72for preset Tff7b72in Ta5d6ff"Te6edf3$presets_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$presetTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$presetTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
Tff7b72done
Tff7b72fi
Tff7b72fi
T8b949e# Priority 3: Extension-provided templates
Tffa657local Te6edf3ext_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/extensionsTa5d6ff"
Tff7b72if Tff7b72[ -d Ta5d6ff"Te6edf3$ext_dirTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tff7b72for ext Tff7b72in Ta5d6ff"Te6edf3$ext_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$extTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
T8b949e# Skip hidden directories (e.g. .backup, .cache)
Tff7b72case Ta5d6ff"Tff7b72$(basename Ta5d6ff"Te6edf3$extTa5d6ff"Tff7b72)Ta5d6ff" Tff7b72in .*Tff7b72) Tff7b72continueTb4b4b4;Tb4b4b4; Tff7b72esac
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$extTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
Tff7b72done
Tff7b72fi
T8b949e# Priority 4: Core templates
Tffa657local Te6edf3coreTff7b72=Ta5d6ff"Te6edf3$baseTa5d6ff/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$coreTa5d6ff" Tff7b72] Tff7b72&& Tffa657echo Ta5d6ff"Te6edf3$coreTa5d6ff" Tff7b72&& Tff7b72return T79c0ff0
T8b949e# Template not found in any location.
T8b949e# Return 1 so callers can distinguish "not found" from "found".
T8b949e# Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
Tff7b72return T79c0ff1
Tff7b72}
T8b949e# Resolve a template name to composed content using composition strategies.
T8b949e# Reads strategy metadata from preset manifests and composes content
T8b949e# from multiple layers using prepend, append, or wrap strategies.
T8b949e#
T8b949e# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
T8b949e# Returns composed content string on stdout; exit code 1 if not found.
resolve_template_contentTff7b72(Tff7b72) Tff7b72{
Tffa657local Te6edf3template_nameTff7b72=Ta5d6ff"Te6edf3$1Ta5d6ff"
Tffa657local Te6edf3repo_rootTff7b72=Ta5d6ff"Te6edf3$2Ta5d6ff"
Tffa657local Te6edf3baseTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/templatesTa5d6ff"
T8b949e# Collect all layers (highest priority first)
Tffa657local -a Te6edf3layer_pathsTff7b72=Tff7b72(Tff7b72)
Tffa657local -a Te6edf3layer_strategiesTff7b72=Tff7b72(Tff7b72)
T8b949e# Priority 1: Project overrides (always "replace")
Tffa657local Te6edf3overrideTff7b72=Ta5d6ff"Te6edf3$baseTa5d6ff/overrides/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$overrideTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$overrideTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"replace"Tff7b72)
Tff7b72fi
T8b949e# Priority 2: Installed presets (sorted by priority from .registry)
Tffa657local Te6edf3presets_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/presetsTa5d6ff"
Tff7b72if Tff7b72[ -d Ta5d6ff"Te6edf3$presets_dirTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3registry_fileTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/.registryTa5d6ff"
Tffa657local Te6edf3sorted_presetsTff7b72=Ta5d6ff""
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$registry_fileTa5d6ff" Tff7b72] Tff7b72&& Tffa657command -v python3 >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
Tff7b72if Te6edf3sorted_presetsTff7b72=Tff7b72$(Te6edf3SPECKIT_REGISTRYTff7b72=Ta5d6ff"Te6edf3$registry_fileTa5d6ff" python3 -c Ta5d6ff"
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" T79c0ff2>/dev/nullTff7b72)Tb4b4b4; Tff7b72then
Tff7b72if Tff7b72[ -n Ta5d6ff"Te6edf3$sorted_presetsTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3yaml_warnedTff7b72=Tffa657false
Tff7b72while Te6edf3IFSTff7b72= Tffa657read -r preset_idTb4b4b4; Tff7b72do
T8b949e# Read strategy and file path from preset manifest
Tffa657local Te6edf3strategyTff7b72=Ta5d6ff"replace"
Tffa657local Te6edf3manifest_fileTff7b72=Ta5d6ff""
Tffa657local Te6edf3manifestTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/Te6edf3$preset_idTa5d6ff/preset.ymlTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$manifestTa5d6ff" Tff7b72] Tff7b72&& Tffa657command -v python3 >/dev/null T79c0ff2>Tb4b4b4&T79c0ff1Tb4b4b4; Tff7b72then
T8b949e# Requires PyYAML; falls back to replace/convention if unavailable
Tffa657local result
Tffa657local py_stderr
Te6edf3py_stderrTff7b72=Tff7b72$(mktempTff7b72)
Te6edf3resultTff7b72=Tff7b72$(Te6edf3SPECKIT_MANIFESTTff7b72=Ta5d6ff"Te6edf3$manifestTa5d6ff" Te6edf3SPECKIT_TMPLTff7b72=Ta5d6ff"Te6edf3$template_nameTa5d6ff" python3 -c Ta5d6ff"
import sys, os
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(os.environ['SPECKIT_MANIFEST']) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
" T79c0ff2>Ta5d6ff"Te6edf3$py_stderrTa5d6ff"Tff7b72)
Tffa657local Te6edf3parse_statusTff7b72=Te6edf3$?
Tff7b72if Tff7b72[ Te6edf3$parse_status -eq T79c0ff0 Tff7b72] Tff7b72&& Tff7b72[ -n Ta5d6ff"Te6edf3$resultTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3IFSTff7b72=Ta5d6ff$'\t' Tffa657read -r strategy manifest_file Tff7b72<<< Ta5d6ff"Te6edf3$resultTa5d6ff"
Te6edf3strategyTff7b72=Tff7b72$(Tffa657printf Ta5d6ff'%s' Ta5d6ff"Te6edf3$strategyTa5d6ff" Tb4b4b4| tr Ta5d6ff'[:upper:]' Ta5d6ff'[:lower:]'Tff7b72)
Tff7b72fi
Tff7b72if Tff7b72[ Ta5d6ff"Te6edf3$yaml_warnedTa5d6ff" Tff7b72= Tffa657false Tff7b72] Tff7b72&& grep -q Ta5d6ff'yaml_missing' Ta5d6ff"Te6edf3$py_stderrTa5d6ff" T79c0ff2>/dev/nullTb4b4b4; Tff7b72then
Tffa657echo Ta5d6ff"Warning: PyYAML not available; composition strategies may be ignored" >Tb4b4b4&T79c0ff2
Te6edf3yaml_warnedTff7b72=Tffa657true
Tff7b72fi
rm -f Ta5d6ff"Te6edf3$py_stderrTa5d6ff"
Tff7b72fi
T8b949e# Try manifest file path first, then convention path
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff""
Tff7b72if Tff7b72[ -n Ta5d6ff"Te6edf3$manifest_fileTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
T8b949e# Reject absolute paths and parent traversal
Tff7b72case Ta5d6ff"Te6edf3$manifest_fileTa5d6ff" Tff7b72in
/*Tb4b4b4|*../*Tb4b4b4|../*Tff7b72) Te6edf3manifest_fileTff7b72=Ta5d6ff"" Tb4b4b4;Tb4b4b4;
Tff7b72esac
Tff7b72fi
Tff7b72if Tff7b72[ -n Ta5d6ff"Te6edf3$manifest_fileTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3mfTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/Te6edf3$preset_idTa5d6ff/Te6edf3$manifest_fileTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$mfTa5d6ff" Tff7b72] Tff7b72&& Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$mfTa5d6ff"
Tff7b72fi
Tff7b72if Tff7b72[ -z Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tffa657local Te6edf3cfTff7b72=Ta5d6ff"Te6edf3$presets_dirTa5d6ff/Te6edf3$preset_idTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72[ -f Ta5d6ff"Te6edf3$cfTa5d6ff" Tff7b72] Tff7b72&& Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$cfTa5d6ff"
Tff7b72fi
Tff7b72if Tff7b72[ -n Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$candidateTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$strategyTa5d6ff"Tff7b72)
Tff7b72fi
Tff7b72done Tff7b72<<< Ta5d6ff"Te6edf3$sorted_presetsTa5d6ff"
Tff7b72fi
Tff7b72else
T8b949e# python3 failed β fall back to unordered directory scan (replace only)
Tff7b72for preset Tff7b72in Ta5d6ff"Te6edf3$presets_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$presetTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$presetTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$candidateTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"replace"Tff7b72)
Tff7b72fi
Tff7b72done
Tff7b72fi
Tff7b72else
T8b949e# No python3 or registry β fall back to unordered directory scan (replace only)
Tff7b72for preset Tff7b72in Ta5d6ff"Te6edf3$presets_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$presetTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$presetTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$candidateTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"replace"Tff7b72)
Tff7b72fi
Tff7b72done
Tff7b72fi
Tff7b72fi
T8b949e# Priority 3: Extension-provided templates (always "replace")
Tffa657local Te6edf3ext_dirTff7b72=Ta5d6ff"Te6edf3$repo_rootTa5d6ff/.specify/extensionsTa5d6ff"
Tff7b72if Tff7b72[ -d Ta5d6ff"Te6edf3$ext_dirTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Tff7b72for ext Tff7b72in Ta5d6ff"Te6edf3$ext_dirTa5d6ff"/*/Tb4b4b4; Tff7b72do
Tff7b72[ -d Ta5d6ff"Te6edf3$extTa5d6ff" Tff7b72] Tff7b72|| Tff7b72continue
Tff7b72case Ta5d6ff"Tff7b72$(basename Ta5d6ff"Te6edf3$extTa5d6ff"Tff7b72)Ta5d6ff" Tff7b72in .*Tff7b72) Tff7b72continueTb4b4b4;Tb4b4b4; Tff7b72esac
Tffa657local Te6edf3candidateTff7b72=Ta5d6ff"Te6edf3$extTa5d6ff/templates/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$candidateTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$candidateTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"replace"Tff7b72)
Tff7b72fi
Tff7b72done
Tff7b72fi
T8b949e# Priority 4: Core templates (always "replace")
Tffa657local Te6edf3coreTff7b72=Ta5d6ff"Te6edf3$baseTa5d6ff/Tffd700${Te6edf3template_nameTffd700}Ta5d6ff.mdTa5d6ff"
Tff7b72if Tff7b72[ -f Ta5d6ff"Te6edf3$coreTa5d6ff" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3layer_pathsTff7b72+=Tff7b72(Ta5d6ff"Te6edf3$coreTa5d6ff"Tff7b72)
Te6edf3layer_strategiesTff7b72+=Tff7b72(Ta5d6ff"replace"Tff7b72)
Tff7b72fi
Tffa657local Te6edf3countTff7b72=Tffd700${#Te6edf3layer_pathsTb4b4b4[@]Tffd700}
Tff7b72[ Ta5d6ff"Te6edf3$countTa5d6ff" -eq T79c0ff0 Tff7b72] Tff7b72&& Tff7b72return T79c0ff1
T8b949e# Check if any layer uses a non-replace strategy
Tffa657local Te6edf3has_compositionTff7b72=Tffa657false
Tff7b72for s Tff7b72in Ta5d6ff"Tffd700${Te6edf3layer_strategiesTb4b4b4[@]Tffd700}Ta5d6ff"Tb4b4b4; Tff7b72do
Tff7b72[ Ta5d6ff"Te6edf3$sTa5d6ff" !Tff7b72= Ta5d6ff"replace" Tff7b72] Tff7b72&& Te6edf3has_compositionTff7b72=Tffa657true Tff7b72&& Tff7b72break
Tff7b72done
T8b949e# If the top (highest-priority) layer is replace, it wins entirely β
T8b949e# lower layers are irrelevant regardless of their strategies.
Tff7b72if Tff7b72[ Ta5d6ff"Tffd700${Te6edf3layer_strategiesTb4b4b4[0]Tffd700}Ta5d6ff" Tff7b72= Ta5d6ff"replace" Tff7b72]Tb4b4b4; Tff7b72then
cat Ta5d6ff"Tffd700${Te6edf3layer_pathsTb4b4b4[0]Tffd700}Ta5d6ff"
Tff7b72return T79c0ff0
Tff7b72fi
Tff7b72if Tff7b72[ Ta5d6ff"Te6edf3$has_compositionTa5d6ff" Tff7b72= Tffa657false Tff7b72]Tb4b4b4; Tff7b72then
cat Ta5d6ff"Tffd700${Te6edf3layer_pathsTb4b4b4[0]Tffd700}Ta5d6ff"
Tff7b72return T79c0ff0
Tff7b72fi
T8b949e# Find the effective base: scan from highest priority (index 0) downward
T8b949e# to find the nearest replace layer. Only compose layers above that base.
Tffa657local Te6edf3base_idxTff7b72=-1
Tffa657local i
Tff7b72for Tff7b72(Tff7b72( Te6edf3iTff7b72=T79c0ff0Tb4b4b4; i<countTb4b4b4; i++ Tff7b72)Tff7b72)Tb4b4b4; Tff7b72do
Tff7b72if Tff7b72[ Ta5d6ff"Tffd700${Te6edf3layer_strategiesTb4b4b4[Te6edf3$iTb4b4b4]Tffd700}Ta5d6ff" Tff7b72= Ta5d6ff"replace" Tff7b72]Tb4b4b4; Tff7b72then
Te6edf3base_idxTff7b72=Te6edf3$i
Tff7b72break
Tff7b72fi
Tff7b72done
Tff7b72if Tff7b72[ Te6edf3$base_idx -lt T79c0ff0 Tff7b72]Tb4b4b4; Tff7b72then
Tff7b72return T79c0ff1 T8b949e# no base layer found
Tff7b72fi
T8b949e# Read the base content; compose layers above the base (higher priority)
Tffa657local content
Te6edf3contentTff7b72=Tff7b72$(cat Ta5d6ff"Tffd700${Te6edf3layer_pathsTb4b4b4[Te6edf3$base_idxTb4b4b4]Tffd700}Ta5d6ff"Tb4b4b4; Tffa657printf xTff7b72)
Te6edf3contentTff7b72=Ta5d6ff"Tffd700${Te6edf3contentTb4b4b4%xTffd700}Ta5d6ff"
Tff7b72for Tff7b72(Tff7b72( Te6edf3iTff7b72=base_idx-1Tb4b4b4; i>Tff7b72=T79c0ff0Tb4b4b4; i-- Tff7b72)Tff7b72)Tb4b4b4; Tff7b72do
Tffa657local Te6edf3pathTff7b72=Ta5d6ff"Tffd700${Te6edf3layer_pathsTb4b4b4[Te6edf3$iTb4b4b4]Tffd700}Ta5d6ff"
Tffa657local Te6edf3stratTff7b72=Ta5d6ff"Tffd700${Te6edf3layer_strategiesTb4b4b4[Te6edf3$iTb4b4b4]Tffd700}Ta5d6ff"
Tffa657local layer_content
T8b949e# Preserve trailing newlines
Te6edf3layer_contentTff7b72=Tff7b72$(cat Ta5d6ff"Te6edf3$pathTa5d6ff"Tb4b4b4; Tffa657printf xTff7b72)
Te6edf3layer_contentTff7b72=Ta5d6ff"Tffd700${Te6edf3layer_contentTb4b4b4%xTffd700}Ta5d6ff"
Tff7b72case Ta5d6ff"Te6edf3$stratTa5d6ff" Tff7b72in
replaceTff7b72) Te6edf3contentTff7b72=Ta5d6ff"Te6edf3$layer_contentTa5d6ff" Tb4b4b4;Tb4b4b4;
prependTff7b72) Te6edf3contentTff7b72=Ta5d6ff"Tff7b72$(Tffa657printf Ta5d6ff'%s\n\n%s' Ta5d6ff"Te6edf3$layer_contentTa5d6ff" Ta5d6ff"Te6edf3$contentTa5d6ff"Tff7b72)Ta5d6ff" Tb4b4b4;Tb4b4b4;
appendTff7b72) Te6edf3contentTff7b72=Ta5d6ff"Tff7b72$(Tffa657printf Ta5d6ff'%s\n\n%s' Ta5d6ff"Te6edf3$contentTa5d6ff" Ta5d6ff"Te6edf3$layer_contentTa5d6ff"Tff7b72)Ta5d6ff" Tb4b4b4;Tb4b4b4;
wrapTff7b72)
Tff7b72case Ta5d6ff"Te6edf3$layer_contentTa5d6ff" Tff7b72in
*Ta5d6ff'{CORE_TEMPLATE}'*Tff7b72) Tb4b4b4;Tb4b4b4;
*Tff7b72) Tffa657echo Ta5d6ff"Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >Tb4b4b4&T79c0ff2Tb4b4b4; Tff7b72return T79c0ff1 Tb4b4b4;Tb4b4b4;
Tff7b72esac
Tff7b72while Tff7b72[Tff7b72[ Ta5d6ff"Te6edf3$layer_contentTa5d6ff" Tff7b72=Tff7b72= *Ta5d6ff'{CORE_TEMPLATE}'* Tff7b72]Tff7b72]Tb4b4b4; Tff7b72do
Tffa657local Te6edf3beforeTff7b72=Ta5d6ff"Tffd700${Te6edf3layer_contentTb4b4b4%%Tffea00\{Te6edf3CORE_TEMPLATETffea00\}Tb4b4b4*Tffd700}Ta5d6ff"
Tffa657local Te6edf3afterTff7b72=Ta5d6ff"Tffd700${Te6edf3layer_contentTb4b4b4#*Tffea00\{Te6edf3CORE_TEMPLATETffea00\}Tffd700}Ta5d6ff"
Te6edf3layer_contentTff7b72=Ta5d6ff"Tffd700${Te6edf3beforeTffd700}Tffd700${Te6edf3contentTffd700}Tffd700${Te6edf3afterTffd700}Ta5d6ff"
Tff7b72done
Te6edf3contentTff7b72=Ta5d6ff"Te6edf3$layer_contentTa5d6ff"
Tb4b4b4;Tb4b4b4;
*Tff7b72) Tffa657echo Ta5d6ff"Ta5d6ffError: unknown strategy 'Te6edf3$stratTa5d6ff'Ta5d6ff" >Tb4b4b4&T79c0ff2Tb4b4b4; Tff7b72return T79c0ff1 Tb4b4b4;Tb4b4b4;
Tff7b72esac
Tff7b72done
Tffa657printf Ta5d6ff'%s' Ta5d6ff"Te6edf3$contentTa5d6ff"
Tff7b72return T79c0ff0
Tff7b72}
Served by rngit 1.5.2 - Generated in 0.1s